本文內容為閱讀有關 Angular 的元件的 lifecycle hook - ngOnDestroy 的筆記內容。
呼叫時機: 當 Angular 要消滅某個 Component 或者 Directive 之前,會呼叫 ngOnDestroy 這個 lifecycle hook。
在官方文件中,有建議我們在 ngOnDestroy 這個 lifecycle hook 做以下這些事情,以防止 memory leak
什麼是 memory leak,根據維基百科的解釋
In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.
以上的內容簡單來說,就是錯誤的管理記憶體的配置,某些不再被用到的記憶體,卻沒有被正確地釋放,這種狀況就會造成 memory leak。
這邊就來說明如果沒有解訂閱 Observable 的話,會怎麼造成記憶體洩漏地狀況。
在許多產品上,時常會有當點擊某個按鈕,就去遠端 server 取得資料,並綁資料呈現在畫面上的操作。
以下來寫個簡單的範例
[子元件 - TypeScript]
import { Component, VERSION } from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
@Component({
selector: 'child',
})
export class AppComponent {
constructor(private authService:AuthService){}
getUserName() {
this.authService
.getUserName()
.subscribe(user => window.alert(`Hello!! ${user.name}`))
}
}
[子元件 - View]
<button type="button" (click)="getUserName">fetch UserData</button>
[父元件 - View]
<child></child>
上面的範例可以看到,當我們點擊子元件的按鈕,他會去遠端取得使用者的資料,並訂閱回傳的 Observable ,並將裡面的使用者姓名呈現在 alert 對話框裡面。
這一切看起來都很合理對吧!! 阿不就你點一次按鈕,就跳一個提示對話框,然後,裡面呈現從遠端取回來的使用者姓名嗎?!
沒錯喔,但是,其實,在每一次完成子元件的 getUserName 函式的內容,我們都會隱性的產出一個 subscription,
哪泥?! 在哪? subscription 就是下圖程式碼會產生出來的東西
所以,每當我們按下一次按鈕就會執行一次 getUserName 內容,接著,產生出一份 subscription ,按下第二次,就產生出第二個 subscription,按越多次,就產生越多 subscription。
等到,這個 child 要被消滅的時候,這些產生出來的 subscription 沒有被退訂閱,就變成了在記憶體宇宙中的太空垃圾,進而造成上面所說的 memory leak 囉~
所以,上面的範例,我們就必須加入 ngOnDestroy 來解決這個問題囉。
改寫的內容如下
[子元件 - TypeScript]
import { Component, OnDestroy} from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
import { Subscription } from 'rxjs';
export class AppComponent implements OnDestroy {
userSub:Subscription
constructor(private authService:AuthService){}
getUserName() {
this.userSub = this.authService
.getUserName()
.subscribe(user => window.alert(`Hello!! ${user.name}`))
}
ngOnDestroy() {
if(this.userSub){
this.userSub.unsubscribe()
}
}
}
ok~~ 是不是蠻輕鬆的呢,要加的內容也不是很多,就是要引入 ngOnDestroy 這個 lifecycle hook 到子元件裡面。
另外,還要引入 RxJs 的 Subscription 來存取我們每一次在 getUserName 函式中攢生出來的 subscription,最後,在 ngOnDestroy 裡面,判斷如果 this.userSub 確實有存取到內容,就將它解訂閱。
如此,就可以防止隱性產生 Subscription 而造成的 memory leak 囉。
上面的範例中的寫法是傳統的寫法。但是它有一個很麻煩的點,當我們有很多 subscription 的話,就得要每一個 subscription 都手動為它加上解訂閱的內容,這樣的話,有一百個 subscription 不就要寫一百次嗎?! 沒錯。
所以,我們會引入 takeUntil 這個 rxjs 的 operator 來優化以上的寫法。
先講一下 takeUntil 的功能,當 takeUntil 裡面的內容接收到值時,就會終止數據流。
讓我們來改寫以下,上面的範例
[子元件 - TypeScript ]
import { Component, OnDestroy} from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export class AppComponent implements OnDestroy {
destroy$ = new Subject<boolean>()
constructor(private authService:AuthService){}
getUserName() {
this.authService
.getUserName()
.pipe(
takeUntil(this.destroy$) // 要寫在 pipe 的最後面
)
.subscribe(user => window.alert(`Hello!! ${user.name}`))
}
ngOnDestroy() {
this.destroy$.next(true)
this.destroy$.unsubscribe()
}
}
以上的範例,改寫的內容,我們不再使用 Subscription 取而代之的是使用 Subject。
step 1.
那我們定義了一個 destroy$ 的 Subject 物件,它要傳入布林值。
step 2.
接著,我們利用 pipe 接在 getUserName 回傳的 Observable 後面。
它的功能就是,當 takeUntil 的 destroy$ 接收到值的時候,就會終止它的數據流。
step3.
ngOnDestroy 的 lifecycle hook 就是我們要啟動 destroy$ 的時機,所以,有看到我們呼叫了它的 next 並傳送了一個 true,此時,每個 subscription 有加上 takeUntil(this.destroy$) 的,都會終止它們的數據流。
最後,我們再解定閱 destroy$ 本身。
經過以上的優化,我們就不用一個一個 subscription 寫它們各自的解訂閱內容了。
在這篇文章中,有提供一個更簡潔的解訂閱寫法。
大意就是,把 destroy$ 和 在 ngOnDestroy 啟動 destroy$ 的內容寫在父元件裡面。子元件的話,就將父元件的內容繼承進來,如此,子元件也能調用屬於父元件的 destroy$,最後,就在那些需要解訂閱的 subscription 加入 takeUntil(destroy$) 的內容。